package io.github.flypiggy.stack.operate.log.spring.boot.starter.aop;


import io.github.flypiggy.stack.operate.log.spring.boot.starter.context.OperatorGetter;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.datasource.DatasourceApi;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.model.LogDTO;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.properties.OperateLogProperties;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.util.StopWatch;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

/**
 * Core Aspect
 */
@Aspect
@Order(-1)
public class LogAspect extends BaseAspect {

    private final Logger log = LoggerFactory.getLogger(LogAspect.class);

    public LogAspect(DatasourceApi datasourceApi,
                     OperateLogProperties operateLogProperties,
                     OperatorGetter operatorGetter) {
        super(datasourceApi, operateLogProperties, operatorGetter);
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping) ||" +
            "@annotation(org.springframework.web.bind.annotation.PostMapping) ||" +
            "@annotation(org.springframework.web.bind.annotation.PutMapping) ||" +
            "@annotation(org.springframework.web.bind.annotation.DeleteMapping) ||" +
            "@annotation(org.springframework.web.bind.annotation.PatchMapping) ||" +
            "@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void restMethod() {
        //do nothing
    }

    /**
     * Core code logic method
     */
    @Around("!@annotation(io.github.flypiggy.stack.operate.log.spring.boot.starter.annotation.UnLog) && restMethod()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        if (Objects.isNull(RequestContextHolder.getRequestAttributes())) {
            log.warn("OPERATE-LOG The method is not a web interface!");
        }
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        if (log.isDebugEnabled()) {
            log.debug("------------------------------------------------------------------------------");
            log.debug("Request source: {}", getIp(request));
            log.debug("Operator:       {}", getOperator());
            log.debug("Request method: {}", request.getMethod());
            log.debug("Api uri:        {}", request.getRequestURI());
            log.debug("Request now:    {}", LocalDateTime.now());
            log.debug("------------------------------------------------------------------------------");
        }
        if (super.isExcludeApi(request.getRequestURI(), request.getMethod())
                || super.isExcludePackageClassMethod(joinPoint)) {
            log.debug("Api exclude. Request method: {}, URI: {}", request.getMethod(), request.getRequestURI());
            stopWatch.stop();
            return joinPoint.proceed();
        }
        LogDTO logDTO = new LogDTO();
        try {
            super.constructBaseLog(logDTO, joinPoint, request);
            if (stopWatch.isRunning()) {
                stopWatch.stop();
                logDTO.setTimeTaken(stopWatch.getTotalTimeMillis());
            }
            Object result = joinPoint.proceed();
            logDTO.setSuccess(true);
            logDTO.setResponseBody(Objects.isNull(result) ? null : OBJECT_MAPPER.writeValueAsString(result));
            return result;
        } catch (Throwable throwable) {
            StackTraceElement[] stackTraceElements = throwable.getStackTrace();
            StackTraceElement traceElement = stackTraceElements[0];
            String errorMessage = String.format("%s\n%s", traceElement, throwable);
            logDTO.setSuccess(false);
            logDTO.setErrorMessage(errorMessage);
            if (super.thrownExceptionNameIsNull || super.thrownExceptionNameSet.contains(throwable.getClass().getSimpleName())) {
                throw throwable;
            }
        } finally {
            CompletableFuture.runAsync(() -> insert(logDTO));
        }
        return null;
    }

}
